# Copyright (c) 2013 ARM Limited
# All rights reserved
#
# The license below extends only to copyright in the software and shall
# not be construed as granting a license to any other intellectual
# property including but not limited to intellectual property relating
# to a hardware implementation of the functionality of the software
# licensed hereunder.  You may use the software subject to the license
# terms below provided that you ensure that this notice is replicated
# unmodified and in its entirety in all distributions of the software,
# modified or unmodified, in source code or in binary form.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met: redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer;
# redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution;
# neither the name of the copyright holders nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

import pygtk

pygtk.require("2.0")
import re

import cairo
import gobject
import gtk

from . import (
    blobs,
    colours,
    model,
    parse,
)
from .model import (
    BlobDataSelect,
    BlobModel,
    Id,
    special_state_chars,
)
from .point import Point


class BlobView:
    """The canvas view of the pipeline"""

    def __init__(self, model):
        # A unit blob will appear at size blobSize inside a space of
        #   size pitch.
        self.blobSize = Point(45.0, 45.0)
        self.pitch = Point(60.0, 60.0)
        self.origin = Point(50.0, 50.0)
        # Some common line definitions to cut down on arbitrary
        #   set_line_widths
        self.thickLineWidth = 10.0
        self.thinLineWidth = 4.0
        self.midLineWidth = 6.0
        # The scale from the units of pitch to device units (nominally
        #   pixels for 1.0 to 1.0
        self.masterScale = Point(1.0, 1.0)
        self.model = model
        self.fillColour = colours.emptySlotColour
        self.timeIndex = 0
        self.time = 0
        self.positions = []
        self.controlbar = None
        # The sequence number selector state
        self.dataSelect = BlobDataSelect()
        # Offset of this view's time from self.time used for miniviews
        #   This is actually an offset of the index into the array of times
        #   seen in the event file)
        self.timeOffset = 0
        # Maximum view size for initial window mapping
        self.initialHeight = 600.0

        # Overlays are speech bubbles explaining blob data
        self.overlays = []

        self.da = gtk.DrawingArea()

        def draw(arg1, arg2):
            self.redraw()

        self.da.connect("expose_event", draw)

        # Handy offsets from the blob size
        self.blobIndent = (self.pitch - self.blobSize).scale(0.5)
        self.blobIndentFactor = self.blobIndent / self.pitch

    def add_control_bar(self, controlbar):
        """Add a BlobController to this view"""
        self.controlbar = controlbar

    def draw_to_png(self, filename):
        """Draw the view to a PNG file"""
        surface = cairo.ImageSurface(
            cairo.FORMAT_ARGB32,
            self.da.get_allocation().width,
            self.da.get_allocation().height,
        )
        cr = gtk.gdk.CairoContext(cairo.Context(surface))
        self.draw_to_cr(cr)
        surface.write_to_png(filename)

    def draw_to_cr(self, cr):
        """Draw to a given CairoContext"""
        cr.set_source_color(colours.backgroundColour)
        cr.set_line_width(self.thickLineWidth)
        cr.paint()
        cr.save()
        cr.scale(*self.masterScale.to_pair())
        cr.translate(*self.origin.to_pair())

        positions = []  # {}

        # Draw each blob
        for blob in self.model.blobs:
            blob_event = self.model.find_unit_event_by_time(
                blob.unit, self.time
            )

            cr.save()
            pos = blob.render(cr, self, blob_event, self.dataSelect, self.time)
            cr.restore()
            if pos is not None:
                (centre, size) = pos
                positions.append((blob, centre, size))

        # Draw all the overlays over the top
        for overlay in self.overlays:
            overlay.show(cr)

        cr.restore()

        return positions

    def redraw(self):
        """Redraw the whole view"""
        buffer = cairo.ImageSurface(
            cairo.FORMAT_ARGB32,
            self.da.get_allocation().width,
            self.da.get_allocation().height,
        )

        cr = gtk.gdk.CairoContext(cairo.Context(buffer))
        positions = self.draw_to_cr(cr)

        # Assume that blobs are in order for depth so we want to
        #   hit the frontmost blob first if we search by position
        positions.reverse()
        self.positions = positions

        # Paint the drawn buffer onto the DrawingArea
        dacr = self.da.window.cairo_create()
        dacr.set_source_surface(buffer, 0.0, 0.0)
        dacr.paint()

        buffer.finish()

    def set_time_index(self, time):
        """Set the time index for the view.  A time index is an index into
        the model's times array of seen event times"""
        self.timeIndex = time + self.timeOffset
        if len(self.model.times) != 0:
            if self.timeIndex >= len(self.model.times):
                self.time = self.model.times[len(self.model.times) - 1]
            else:
                self.time = self.model.times[self.timeIndex]
        else:
            self.time = 0

    def get_pic_size(self):
        """Return the size of ASCII-art picture of the pipeline scaled by
        the blob pitch"""
        return self.origin + self.pitch * (
            self.model.picSize + Point(1.0, 1.0)
        )

    def set_da_size(self):
        """Set the DrawingArea size after scaling"""
        self.da.set_size_request(10, int(self.initialHeight))


class BlobController:
    """The controller bar for the viewer"""

    def __init__(
        self, model, view, defaultEventFile="", defaultPictureFile=""
    ):
        self.model = model
        self.view = view
        self.playTimer = None
        self.filenameEntry = gtk.Entry()
        self.filenameEntry.set_text(defaultEventFile)
        self.pictureEntry = gtk.Entry()
        self.pictureEntry.set_text(defaultPictureFile)
        self.timeEntry = None
        self.defaultEventFile = defaultEventFile
        self.startTime = None
        self.endTime = None

        self.otherViews = []

        def make_bar(elems):
            box = gtk.HBox(homogeneous=False, spacing=2)
            box.set_border_width(2)
            for widget, signal, handler in elems:
                if signal is not None:
                    widget.connect(signal, handler)
                box.pack_start(widget, False, True, 0)
            return box

        self.timeEntry = gtk.Entry()

        t = gtk.ToggleButton("T")
        t.set_active(False)
        s = gtk.ToggleButton("S")
        s.set_active(True)
        p = gtk.ToggleButton("P")
        p.set_active(True)
        l = gtk.ToggleButton("L")
        l.set_active(True)
        f = gtk.ToggleButton("F")
        f.set_active(True)
        e = gtk.ToggleButton("E")
        e.set_active(True)

        # Should really generate this from above
        self.view.dataSelect.ids = set("SPLFE")

        self.bar = gtk.VBox()
        self.bar.set_homogeneous(False)

        row1 = make_bar(
            [
                (gtk.Button("Start"), "clicked", self.time_start),
                (gtk.Button("End"), "clicked", self.time_end),
                (gtk.Button("Back"), "clicked", self.time_back),
                (gtk.Button("Forward"), "clicked", self.time_forward),
                (gtk.Button("Play"), "clicked", self.time_play),
                (gtk.Button("Stop"), "clicked", self.time_stop),
                (self.timeEntry, "activate", self.time_set),
                (gtk.Label("Visible ids:"), None, None),
                (t, "clicked", self.toggle_id("T")),
                (gtk.Label("/"), None, None),
                (s, "clicked", self.toggle_id("S")),
                (gtk.Label("."), None, None),
                (p, "clicked", self.toggle_id("P")),
                (gtk.Label("/"), None, None),
                (l, "clicked", self.toggle_id("L")),
                (gtk.Label("/"), None, None),
                (f, "clicked", self.toggle_id("F")),
                (gtk.Label("."), None, None),
                (e, "clicked", self.toggle_id("E")),
                (self.filenameEntry, "activate", self.load_events),
                (gtk.Button("Reload"), "clicked", self.load_events),
            ]
        )

        self.bar.pack_start(row1, False, True, 0)
        self.set_time_index(0)

    def toggle_id(self, id):
        """One of the sequence number selector buttons has been toggled"""

        def toggle(button):
            if button.get_active():
                self.view.dataSelect.ids.add(id)
            else:
                self.view.dataSelect.ids.discard(id)

            # Always leave one thing visible
            if len(self.view.dataSelect.ids) == 0:
                self.view.dataSelect.ids.add(id)
                button.set_active(True)
            self.view.redraw()

        return toggle

    def set_time_index(self, time):
        """Set the time index in the view"""
        self.view.set_time_index(time)

        for view in self.otherViews:
            view.set_time_index(time)
            view.redraw()

        self.timeEntry.set_text(str(self.view.time))

    def time_start(self, button):
        """Start pressed"""
        self.set_time_index(0)
        self.view.redraw()

    def time_end(self, button):
        """End pressed"""
        self.set_time_index(len(self.model.times) - 1)
        self.view.redraw()

    def time_forward(self, button):
        """Step forward pressed"""
        self.set_time_index(
            min(self.view.timeIndex + 1, len(self.model.times) - 1)
        )
        self.view.redraw()
        gtk.gdk.flush()

    def time_back(self, button):
        """Step back pressed"""
        self.set_time_index(max(self.view.timeIndex - 1, 0))
        self.view.redraw()

    def time_set(self, entry):
        """Time dialogue changed.  Need to find a suitable time
        <= the entry's time"""
        newTime = self.model.find_time_index(int(entry.get_text()))
        self.set_time_index(newTime)
        self.view.redraw()

    def time_step(self):
        """Time step while playing"""
        if (
            not self.playTimer
            or self.view.timeIndex == len(self.model.times) - 1
        ):
            self.time_stop(None)
            return False
        else:
            self.time_forward(None)
            return True

    def time_play(self, play):
        """Automatically advance time every 100 ms"""
        if not self.playTimer:
            self.playTimer = gobject.timeout_add(100, self.time_step)

    def time_stop(self, play):
        """Stop play pressed"""
        if self.playTimer:
            gobject.source_remove(self.playTimer)
            self.playTimer = None

    def load_events(self, button):
        """Reload events file"""
        self.model.load_events(
            self.filenameEntry.get_text(),
            startTime=self.startTime,
            endTime=self.endTime,
        )
        self.set_time_index(
            min(len(self.model.times) - 1, self.view.timeIndex)
        )
        self.view.redraw()


class Overlay:
    """An Overlay is a speech bubble explaining the data in a blob"""

    def __init__(self, model, view, point, blob):
        self.model = model
        self.view = view
        self.point = point
        self.blob = blob

    def find_event(self):
        """Find the event for a changing time and a fixed blob"""
        return self.model.find_unit_event_by_time(
            self.blob.unit, self.view.time
        )

    def show(self, cr):
        """Draw the overlay"""
        event = self.find_event()

        if event is None:
            return

        insts = event.find_ided_objects(self.model, self.blob.picChar, False)

        cr.set_line_width(self.view.thinLineWidth)
        cr.translate(*(Point(0.0, 0.0) - self.view.origin).to_pair())
        cr.scale(*(Point(1.0, 1.0) / self.view.masterScale).to_pair())

        # Get formatted data from the insts to format into a table
        lines = list(inst.table_line() for inst in insts)

        text_size = 10.0
        cr.set_font_size(text_size)

        def text_width(str):
            xb, yb, width, height, dx, dy = cr.text_extents(str)
            return width

        # Find the maximum number of columns and the widths of each column
        num_columns = 0
        for line in lines:
            num_columns = max(num_columns, len(line))

        widths = [0] * num_columns
        for line in lines:
            for i in range(len(line)):
                widths[i] = max(widths[i], text_width(line[i]))

        # Calculate the size of the speech bubble
        column_gap = 1 * text_size
        id_width = 6 * text_size
        total_width = sum(widths) + id_width + column_gap * (num_columns + 1)
        gap_step = Point(1.0, 0.0).scale(column_gap)

        text_point = self.point
        text_step = Point(0.0, text_size)

        size = Point(total_width, text_size * len(insts))

        # Draw the speech bubble
        blobs.speech_bubble(cr, self.point, size, text_size)
        cr.set_source_color(colours.backgroundColour)
        cr.fill_preserve()
        cr.set_source_color(colours.black)
        cr.stroke()

        text_point += Point(1.0, 1.0).scale(2.0 * text_size)

        id_size = Point(id_width, text_size)

        # Draw the rows in the table
        for i in range(0, len(insts)):
            row_point = text_point
            inst = insts[i]
            line = lines[i]
            blobs.striped_box(
                cr,
                row_point + id_size.scale(0.5),
                id_size,
                inst.id.to_striped_block(self.view.dataSelect),
            )
            cr.set_source_color(colours.black)

            row_point += Point(1.0, 0.0).scale(id_width)
            row_point += text_step
            # Draw the columns of each row
            for j in range(0, len(line)):
                row_point += gap_step
                cr.move_to(*row_point.to_pair())
                cr.show_text(line[j])
                row_point += Point(1.0, 0.0).scale(widths[j])

            text_point += text_step


class BlobWindow:
    """The top-level window and its mouse control"""

    def __init__(self, model, view, controller):
        self.model = model
        self.view = view
        self.controller = controller
        self.controlbar = None
        self.window = None
        self.miniViewCount = 0

    def add_control_bar(self, controlbar):
        self.controlbar = controlbar

    def show_window(self):
        self.window = gtk.Window()

        self.vbox = gtk.VBox()
        self.vbox.set_homogeneous(False)
        if self.controlbar:
            self.vbox.pack_start(self.controlbar, False, True, 0)
        self.vbox.add(self.view.da)

        if self.miniViewCount > 0:
            self.miniViews = []
            self.miniViewHBox = gtk.HBox(homogeneous=True, spacing=2)

            # Draw mini views
            for i in range(1, self.miniViewCount + 1):
                miniView = BlobView(self.model)
                miniView.set_time_index(0)
                miniView.masterScale = Point(0.1, 0.1)
                miniView.set_da_size()
                miniView.timeOffset = i + 1
                self.miniViews.append(miniView)
                self.miniViewHBox.pack_start(miniView.da, False, True, 0)

            self.controller.otherViews = self.miniViews
            self.vbox.add(self.miniViewHBox)

        self.window.add(self.vbox)

        def show_event(picChar, event):
            print("**** Comments for", event.unit, "at time", self.view.time)
            for name, value in event.pairs.items():
                print(name, "=", value)
            for comment in event.comments:
                print(comment)
            if picChar in event.visuals:
                # blocks = event.visuals[picChar].elems()
                print("**** Colour data")
                objs = event.find_ided_objects(self.model, picChar, True)
                for obj in objs:
                    print(" ".join(obj.table_line()))

        def clicked_da(da, b):
            point = Point(b.x, b.y)

            overlay = None
            for blob, centre, size in self.view.positions:
                if point.is_within_box((centre, size)):
                    event = self.model.find_unit_event_by_time(
                        blob.unit, self.view.time
                    )
                    if event is not None:
                        if overlay is None:
                            overlay = Overlay(
                                self.model, self.view, point, blob
                            )
                        show_event(blob.picChar, event)
            if overlay is not None:
                self.view.overlays = [overlay]
            else:
                self.view.overlays = []

            self.view.redraw()

        # Set initial size and event callbacks
        self.view.set_da_size()
        self.view.da.add_events(gtk.gdk.BUTTON_PRESS_MASK)
        self.view.da.connect("button-press-event", clicked_da)
        self.window.connect("destroy", lambda widget: gtk.main_quit())

        def resize(window, event):
            """Resize DrawingArea to match new window size"""
            size = Point(float(event.width), float(event.height))
            proportion = size / self.view.get_pic_size()
            # Preserve aspect ratio
            daScale = min(proportion.x, proportion.y)
            self.view.masterScale = Point(daScale, daScale)
            self.view.overlays = []

        self.view.da.connect("configure-event", resize)

        self.window.show_all()
